package rdb;

import tcp.Server;

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;

public class RDBWriter {
    private BufferedOutputStream rdbWriter;
    private CRC crcChecker;
    private long crcVal;

    public RDBWriter() throws IOException  {
        rdbWriter = new BufferedOutputStream(new FileOutputStream(Server.getServerConfig().rdbFileName));
        crcChecker = new CRC(CRC.Parameters.CRC64ISO);
        crcVal = crcChecker.init();
    }

    public void writeData(byte[] data) throws IOException {
        rdbWriter.write(data);
        crcVal = crcChecker.update(crcVal, data);
    }
    
    public void flush() throws IOException {
        rdbWriter.flush();
    }

    public void closeWriter() throws IOException {
        rdbWriter.close();
    }
    public void writeHeader() throws IOException {
        writeData("REDIS0009".getBytes(StandardCharsets.UTF_8));
    }

    public void writeAux(String key, String val) throws IOException {
        writeData(new byte[]{(byte) RDBConstant.OP_CODE_AUX});
        writeString(key);
        writeString(val);
    }

    public void writeDBHeader(int dbIndex, int keyCount, int ttlCount) throws IOException {
        writeData(new byte[]{(byte) RDBConstant.OP_CODE_SELECT_DB});
        writeLength(dbIndex);
        writeData(new byte[]{(byte) RDBConstant.OP_CODE_RESIZE_DB});

        writeLength(keyCount);
        writeLength(ttlCount);
    }



    public void writeString(String s) {
        // 尝试按照数字处理
        try {
            if (tryWriterNumber(s)) {
                return;
            }
            // 字符串长度 > 20 字节，使用 LZF 算法压缩保存
            if (s.getBytes(StandardCharsets.UTF_8).length > 20) {
                writeLZF(s);
                return;
            }
            writeSimpleString(s);
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }
    }

    public void writeBytes(byte[] bs) throws IOException {
        writeLength(bs.length);
        writeData(bs);
    }

    public void writeSimpleString(String s) throws IOException {
        writeLength(s.length());
        // 如果传入的 s 的底层 byte[] 并不是 ascii 字符，那么再解码成 byte[] 后，会和原来的 byte[] 不一样，建议直接使用 writeBytes 写入
        writeData(s.getBytes(StandardCharsets.UTF_8));
    }

    public void writeLZF(String s) throws IOException {
        byte[] out = LZFCompress(s.getBytes(StandardCharsets.UTF_8));
        if (out == null) {
            return;
        }

        writeData(new byte[]{(byte)RDBConstant.ENCODE_LZF});
        writeLength(out.length);
        writeLength(s.length());
        writeData(out);
    }

    public byte[] LZFCompress(byte[] input) {
        int hval, ref, hslot, off;
        int outputIndex = 0, inputIndex = 0, lit = 0;
        byte[] output = new byte[input.length];
        int[] htab = new int[RDBConstant.HTAB_SIZE];
        int len = 2, maxLen = 0;

        if (input.length == 0) {
            return null;
        }

        lit = 0;
        outputIndex++;

        hval = (int)input[inputIndex] << 8 | (int)input[inputIndex+1];
        while (inputIndex < input.length - 2) {
            hval = hval << 8 | ((int) input[inputIndex+2]);
            hslot = ((hval >> (3*8 - RDBConstant.HTAB_LOG)) - hval*5) & (RDBConstant.HTAB_SIZE - 1);
            ref = htab[hslot];
            htab[hslot] = inputIndex;
            off = inputIndex - ref - 1;

            if (off < RDBConstant.MAX_OFF && ref > 0 &&
                input[ref] == input[inputIndex] &&
                input[ref+1] == input[inputIndex+1] &&
                input[ref+2] == input[inputIndex+2]) {

                len = 2;
                maxLen = input.length - inputIndex - len;
                if (maxLen > RDBConstant.MAX_REF) {
                    maxLen = RDBConstant.MAX_REF;
                }

                if (outputIndex+3+1 >= output.length) { /* first a faster conservative test */
                    int nlit = 0;
                    if (lit == 0) {
                        nlit = 1;
                    }
                    if (outputIndex-nlit+3+1 >= output.length) { /* second the exact but rare test */
                        return null;
                    }
                }

                output[outputIndex-lit-1] = (byte)(lit - 1); /* stop run */
                if (lit == 0) {
                    outputIndex--; /* undo run if length is zero */
                }

                while (true) {
                    len++;
                    if ((len >= maxLen) || (input[(int)(ref)+len] != input[inputIndex+len])) {
                        break;
                    }
                }

                len -= 2; /* len is now #octets - 1 */
                inputIndex++;

                if (len < 7) {
                    output[outputIndex] = (byte)((off >> 8) + (len<<5));
                    outputIndex++;
                } else {
                    output[outputIndex] = (byte)((off >> 8) + (7 << 5));
                    output[outputIndex+1] = (byte)(len - 7);
                    outputIndex += 2;
                }

                output[outputIndex] = (byte)(off);
                outputIndex += 2;
                lit = 0; /* start run */

                inputIndex += len + 1;

                if (inputIndex >= input.length-2) {
                    break;
                }

                inputIndex -= 2;

                hval = (int)(input[inputIndex])<<8 | (int)(input[inputIndex+1]);
                hval = (hval << 8) | (int)(input[inputIndex+2]);
                hslot = ((hval >> (3*8 - RDBConstant.HTAB_LOG)) - (hval * 5)) & (RDBConstant.HTAB_SIZE - 1);
                htab[hslot] = inputIndex;
                inputIndex++;

                hval = (hval << 8) | (int)(input[inputIndex+2]);
                hslot = ((hval >> (3*8 - RDBConstant.HTAB_LOG)) - (hval * 5)) & (RDBConstant.HTAB_SIZE - 1);
                htab[hslot] = inputIndex;
                inputIndex++;

            } else {
                /* one more literal byte we must copy */
                if (outputIndex >= output.length) {
                    return null;
                }

                lit++;
                output[outputIndex] = input[inputIndex];
                outputIndex++;
                inputIndex++;

                if (lit == RDBConstant.MAX_LIT) {
                    output[outputIndex-lit-1] = (byte)(lit - 1); /* stop run */
                    lit = 0;                                   /* start run */
                    outputIndex++;
                }
            }

        }

        if (outputIndex+3 >= output.length) { /* at most 3 bytes can be missing here */
            return null;
        }

        while (inputIndex < input.length) {
            lit++;
            output[outputIndex] = input[inputIndex];
            outputIndex++;
            inputIndex++;

            if (lit == RDBConstant.MAX_LIT) {
                output[outputIndex-lit-1] = (byte)(lit - 1); /* stop run */
                lit = 0;                                   /* start run */
                outputIndex++;
            }
        }

        output[outputIndex-lit-1] = (byte)(lit - 1); /* end run */
        if (lit == 0) {                             /* undo run if length is zero */
            outputIndex--;
        }

        return Arrays.copyOfRange(output, 0, outputIndex);
    }

    public boolean tryWriterNumber(String s) throws IOException {
        try {
            long val = Long.parseLong(s);
            if (val <= Byte.MAX_VALUE) {
                writeData(new byte[]{(byte) RDBConstant.ENCODE_8_BIT, (byte) val});
            } else if (val <= Short.MAX_VALUE) {
                writeData(new byte[]{(byte) RDBConstant.ENCODE_16_BIT, (byte) val, (byte) (val >> 8)});
            } else if (val <= Integer.MAX_VALUE) {
                byte[] bs = new byte[5];
                bs[0] = (byte) RDBConstant.ENCODE_32_BIT;
                for (int i=1;i<5;i++) {
                    bs[i] = (byte) (val >> ((i-1)*8));
                }
                writeData(bs);
            }
            return true;
        } catch(NumberFormatException e) {
            return false;
        }
    }

    public void writeLength(long dataLength) throws IOException {
        byte[] ret = null;
        if (dataLength <= RDBConstant.RANGE_6_BITS) {
            ret = new byte[]{(byte) dataLength};
        } else if (dataLength <= RDBConstant.RANGE_14_BITS) {
            ret = new byte[2];
            ret[0] = (byte) ((dataLength >>> 8) | RDBConstant.LEN_14_BIT_MASK);
            ret[1] = (byte) dataLength;
        } else if (dataLength <= RDBConstant.RANGE_32_BITS) {
            ret = new byte[5];
            ret[0] = (byte) 0x80;
            for (int i=1;i<5;i++) {
                ret[i] = (byte) (dataLength >>> (32-i*8));
            }
        } else {
            // 64 bits
            ret = new byte[9];
            ret[0] = (byte) 0x81;
            for (int i=1;i<9;i++) {
                ret[i] = (byte) (dataLength >> (64-i*8));
            }
        }
        writeData(ret);
    }

    public void writeTTL(Long expiration) throws IOException {
        if (expiration == -1) {
            return;
        }
        writeData(new byte[]{(byte) RDBConstant.OP_CODE_EXPIRE_TIME_MS});
        writeData(RDBParser.longToBytes(expiration));
    }

    public void writeStringObject(String key, String val, long expiration) throws IOException {
        writeTTL(expiration);
        writeData(new byte[]{(byte) RDBConstant.TYPE_STRING});
        writeString(key);
        writeString(val);
    }

    public void writeZipList(String[] vals) throws IOException {
        // header = zlBytes 4B zlTail 4B zlLen 2B 总共 10 B
        // zlEnd = 1B
        int zlBytes = 11;
        int zlTail = 0;
        int preLen = 0;
        // 因为当列表项小于 512 项且每个项的大小都要小于 64 个字节，才会用 zip list
        byte[] buffer = new byte[1024*64];
        byte[] entryBs = null;
        // 先给 zip list header 留出 10B 空间
        int cursor = 10;

        for (int i=0;i<vals.length;i++) {
            entryBs = encodeZipListEntry(preLen, vals[i]);
            for (byte entryB : entryBs) {
                buffer[cursor++] = entryB;
            }
            preLen = entryBs.length;
            zlBytes += entryBs.length;
            if (i < vals.length-1) {
                zlTail += entryBs.length;
            }
        }

        // 写入 EOF
        buffer[cursor++] = (byte) 0xff;

        // 写入 zip list header
        // 写入 zlBytes, 小端序
        int j = 0;
        for (j=0;j<4;j++) {
            buffer[j] = (byte) (zlBytes >> (j*8));
        }

        // 写入 zlTail
        int m = 0;
        for (j=4;j<8;j++,m++) {
            buffer[j] = (byte) (zlTail >> (m*8));
        }

        // 写入 zlLen
        m = 0;
        int zlLen = vals.length;
        for (j=8;j<10;j++,m++) {
            buffer[j] = (byte) (zlLen >> (m*8));
        }
        byte[] temp = Arrays.copyOf(buffer, cursor);
        writeBytes(temp);
    }

    public void writeNanString(String s) throws IOException {
        if (s.length() > 20) {
            writeLZF(s);
            return;
        }
        writeSimpleString(s);
    }

    private byte[] encodeZipListEntry(int preLen, String val) {
        byte[] ret = new byte[2048];
        int cursor = 0;
        //  当,前一个节点长度 < 254 时，只用 1个字节表示长度，否则是 5 个字节
        if (preLen < 0xFE) {
            ret[cursor++] = (byte) (preLen & 0xff);
        } else {
            ret[cursor++] = (byte) 0xFE;
            for (int i=0;i<4;i++) {
                ret[cursor++] = (byte) (preLen >>> (i*8));
            }
        }
        // 先假设是个数字
        try {
            long numVal = Long.parseLong(val);
            if (numVal >= 0 && numVal <= 12) {
                System.out.printf("numVal: %d\n", numVal);
                numVal++;
                ret[cursor++] = (byte) (0xf0 | (numVal));
            } else if (numVal >= Byte.MIN_VALUE && numVal <= Byte.MAX_VALUE) {
                ret[cursor++] = (byte) 0xFE;
                ret[cursor++] = (byte) numVal;
            } else if (numVal >= Short.MIN_VALUE && numVal <= Short.MAX_VALUE) {
                ret[cursor++] = (byte) 0xC0;
                ret[cursor++] = (byte) numVal;
                ret[cursor++] = (byte) (numVal >> 8);
            } else if (numVal >= RDBConstant.MIN_24_BIT && numVal <= RDBConstant.MAX_24_BIT) {
                ret[cursor++] = (byte) 0xF0;
                for (int i=0;i<3;i++) {
                    ret[cursor++] = (byte) (numVal >> (i*8));
                }
            } else if (numVal >= Integer.MIN_VALUE && numVal <= Integer.MAX_VALUE) {
                ret[cursor++] = (byte) 0xD0;
                for (int i=0;i<4;i++) {
                    ret[cursor++] = (byte) (numVal >> (i*8));
                }
            } else {
                ret[cursor++] = (byte) 0xE0;
                for (int i=0;i<8;i++) {
                    ret[cursor++] = (byte) (numVal >> (i*8));
                }
            }
        } catch (NumberFormatException e) {
            System.out.println("val: " + val);
            // 不是一个数字，是一个其他的字符串
            int valLen = val.length();
            if (valLen <= 63) {
                ret[cursor++] = (byte) val.length();
            } else if (valLen <= RDBConstant.MAX_14_BIT) {
                // 这里用的是大端序
                ret[cursor++] = (byte) ((val.length() >> 8) | RDBConstant.LEN_14_BIT_MASK);
                ret[cursor++] = (byte) val.length();
            } else {
                // encoding
                ret[cursor++] = (byte) 0x80;
                // 这里采用大端序
                for (int i=1;i<=4;i++) {
                    ret[cursor++] = (byte) (val.length() >> ((4-i)*8));
                }
            }
            byte[] contentBs = val.getBytes(StandardCharsets.UTF_8);
            for (int i=0;i<contentBs.length;i++) {
                ret[cursor++] = contentBs[i];
            }
            System.out.println("return in catch");
            return Arrays.copyOf(ret, cursor);
        }
        return Arrays.copyOf(ret, cursor);
    }

    public void writeListZipList(String key, String[] vals, long expiration) throws IOException {
        writeTTL(expiration);
        writeData(new byte[]{(byte) RDBConstant.TYPE_LIST_ZIP_LIST});
        writeString(key);
        writeZipList(vals);
    }

    /*
    quick list 是包含多个 zip list 的 list
     */
    public void writeQuickList(String key, String[] vals, long expiration) throws IOException {
        writeTTL(expiration);
        ArrayList<String> page = new ArrayList<>();
        ArrayList<ArrayList<String>> pages = new ArrayList<>();
        int pageSize = 0;
        for (int i = 0; i < vals.length; i++) {
            page.add(vals[i]);
            pageSize += vals[i].length();
            if (pageSize >= 4*1024) {
                pages.add(page);
                page = new ArrayList<>();
            }
        }
        if (page.size() > 0) {
            pages.add(page);
        }

        writeData(new byte[]{(byte) RDBConstant.TYPE_LIST_QUICK_LIST});
        writeString(key);
        writeLength(pages.size());
        for(ArrayList<String> p : pages) {
            writeZipList(p.toArray(new String[0]));
        }
    }

    public boolean checkListItem(String[] listVals) {
        for (int i = 0; i < listVals.length; i++) {
            if (listVals[i].length() >= 64) {
                return false;
            }
        }
        return true;
    }

    public void writeListObject(String key, String[] vals, long expiration) throws IOException {
        // 如果列表项 < 512 就用 zip list 编码
        if (vals.length < 512 && checkListItem(vals)) {
            writeListZipList(key, vals, expiration);
        } else {
            writeQuickList(key, vals, expiration);
        }
    }

    public boolean tryWriteIntSetEncoding(String key, String[] vals, long expiration) throws IOException {
        long maxLong = Long.MIN_VALUE;
        long minLong = Long.MAX_VALUE;

        long[] valList = new long[vals.length];
        for (int i=0;i<valList.length;i++) {
            try {
                long numVal = Long.parseLong(vals[i]);
                if (numVal > maxLong) {
                    maxLong = numVal;
                } else if (numVal < minLong) {
                    minLong = numVal;
                }
                valList[i] = numVal;
            } catch (NumberFormatException e) {
                return false;
            }
        }

        if (valList.length > 512) {
            return false;
        }

        int intSize = 8;
        if (minLong >= Short.MIN_VALUE && maxLong <= Short.MAX_VALUE) {
            intSize = 2;
        } else if (minLong >= Integer.MIN_VALUE && maxLong <= Integer.MAX_VALUE) {
            intSize = 4;
        }

        // int set 是升序排列
        Arrays.sort(valList);

        writeTTL(expiration);
        writeData(new byte[]{(byte) RDBConstant.TYPE_SET_INT_SET});
        writeString(key);
        byte[] buf = new byte[8 + intSize * vals.length];
        // 写入 int size 4B, 小端序
        for (int i=0;i<4;i++) {
            buf[i] = (byte) (intSize >> (i*8));
        }

        // 写入 set 中的元素数量, 小端序
        int cur = 4;
        for (int i=0;i<4;i++) {
            buf[cur++] = (byte) (vals.length >> (i*8));
        }

        // 写入具体元素，小端序
        for (int i=0;i<valList.length;i++) {
            for (int j=0;j<intSize;j++) {
                buf[cur++] = (byte) (valList[i] >> (j*8));
            }
        }

        writeBytes(buf);
//        writeNanString(new String(buf));
        return true;
    }

    public void writeSetObject(String key, String[] vals, long expiration) throws IOException {
        // set 对象的底层编码类型可以是 intset 也可以是 hashtable
        // 满足下列这两个条件时，就使用 intset，否则就用 hashtable
        // 1、所有元素都是整数
        // 2、元素数量 <= 512
        if (tryWriteIntSetEncoding(key, vals, expiration)) {
            return;
        }

        /*
        ListEncoding 开头为一个普通长度编码块表示 List 的长度，随后是对应个数的 StringEncoding 块。
        SetEncoding 与 ListEncoding 完全相同
         */
        writeTTL(expiration);
        writeData(new byte[]{RDBConstant.TYPE_SET});
        writeString(key);
        writeLength(vals.length);
        for (String val : vals) {
            writeString(val);
        }
    }

    public boolean tryWriteZipListHashMap(String key, Map<String, String> hash, long expiration) throws IOException {
        /*
        当 hash 对象同时满足下列两个条件时，hash 对象使用 zip list 编码
        1、所有键值对的字符串长度 < 64B
        2、键值对数量 < 512
        如果不满足以上条件，就用 hashtable 编码
         */
        if (hash.size() >= 512) {
            return false;
        }

        for (Map.Entry<String, String> entry : hash.entrySet()) {
            if (entry.getKey().length() >= 64 || entry.getValue().length() >= 64) {
                return false;
            }
        }

        writeData(new byte[]{RDBConstant.TYPE_HASH_ZIP_LIST});
        writeString(key);
        String[] entries = new String[hash.size()*2];

        int i = 0;
        for (Map.Entry<String, String> entry : hash.entrySet()) {
            entries[2*i] = entry.getKey();
            entries[2*i+1] = entry.getValue();
            System.out.printf("### entry key: %s, entry value: %s\n", entry.getKey(), entry.getValue());
            i++;
        }

        writeZipList(entries);
        return true;
    }

    public void writeHashMapObject(String key, Map<String, String> hash, long expiration) throws IOException {
        /*
        当 hash 对象同时满足下列两个条件时，hash 对象使用 zip list 编码
        1、所有键值对的字符串长度 < 64B
        2、键值对数量 < 512
        如果不满足以上条件，就用 hashtable 编码
         */
        writeTTL(expiration);
        if (tryWriteZipListHashMap(key, hash, expiration)) {
            return;
        }
        writeHashEncoding(key, hash, expiration);
    }

    /*
    HashEncoding 开头为一个普通长度编码块表示哈希表中的键值对个数，
    随后为对应个数的：Key StringEncoding + Value StringEncoding 组成的键值对。
     */
    public void writeHashEncoding(String key, Map<String, String> hash, long expiration) throws IOException {
        writeData(new byte[]{RDBConstant.TYPE_HASH});
        writeString(key);
        writeLength(hash.size());

        for (Map.Entry<String, String> entry : hash.entrySet()) {
            writeString(entry.getKey());
            writeString(entry.getValue());
        }
    }

    /*
    这两种表示有序集合方式非常类似，开头是一个普通长度编码块表示元素数，
    随后是对应个数的表示score的float值 + 表示 member 的 StringEncode。
    唯一的区别是，ZSet 的 score 采用字符串来存储浮点数，ZSet2 使用 IEEE 754 规定的二进制格式存储 float.

    有序集合的编码方式可以是 zipList 或者 skipList
    当有序集合满足下列两个条件时，有序集合使用 zipList 编码
    1、元素数量 < 512
    2、所有元素成员的大小 < 64B
     */
    public void writeZSetObject(String key, String[] members, double[] scores, long expiration) throws IOException {
        writeTTL(expiration);
        if (tryWriteZipListZSet(key, members, scores)) {
            return;
        }
        writeZSet2Encoding(key, members, scores);
    }

    public void writeZSet2Encoding(String key, String[] members, double[] scores) throws IOException {
        writeData(new byte[]{RDBConstant.TYPE_ZSET2});
        writeString(key);
        writeLength(members.length);

        for (int i=0;i<members.length;i++) {
            writeString(members[i]);
            writeDouble(scores[i]);
        }
    }

    public void writeDouble(double num) throws IOException {
        long longBits = Double.doubleToRawLongBits(num);
        byte[] buf = new byte[8];
        for (int i=0;i<8;i++) {
            buf[i] = (byte) (longBits >> (i*8));
        }
        writeData(buf);
    }
    private boolean tryWriteZipListZSet(String key, String[] members, double[] scores) throws IOException {
        if (members.length >= 512) {
            return false;
        }

        for (String member : members) {
            if (member.length() >= 64) {
                return false;
            }
        }

        writeData(new byte[]{RDBConstant.TYPE_ZSET_ZIP_LIST});
        writeString(key);
        String[] zlElements = new String[members.length+scores.length];
        for (int i=0;i<members.length;i++) {
            String scoreStr = Double.toString(scores[i]);
            zlElements[2*i] = members[i];
            zlElements[2*i+1] = scoreStr;
        }
        writeZipList(zlElements);
        return true;
    }

    public void writeEnd() throws IOException {
        writeData(new byte[]{(byte) RDBConstant.OP_CODE_EOF});
        // 写入校验和
        long checkSum = crcChecker.finalCRC(crcVal);
        byte[] checkSumBytes = new byte[8];
        // 小端序
        for (int i=0;i<8;i++) {
            checkSumBytes[i] = (byte) (checkSum >> (i*8));
        }
        rdbWriter.write(checkSumBytes);
        // write LF
        rdbWriter.write(new byte[]{(byte) 0x0a});
    }
}


